// Copyright (c) 2022. Eritque arcus and contributors.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or any later version(in your opinion).
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

#pragma once

#include <cstddef>  // size_t
#include <iterator> // input_iterator_tag
#include <string>   // string, to_string
#include <tuple>    // tuple_size, get, tuple_element
#include <utility>  // move

#if JSON_HAS_RANGES
#include <ranges> // enable_borrowed_range
#endif

#include <nlohmann/detail/abi_macros.hpp>
#include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/value_t.hpp>

NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail {

    template<typename string_type>
    void int_to_string(string_type &target, std::size_t value) {
        // For ADL
        using std::to_string;
        target = to_string(value);
    }
    template<typename IteratorType>
    class iteration_proxy_value {
    public:
        using difference_type = std::ptrdiff_t;
        using value_type = iteration_proxy_value;
        using pointer = value_type *;
        using reference = value_type &;
        using iterator_category = std::input_iterator_tag;
        using string_type = typename std::remove_cv<typename std::remove_reference<decltype(std::declval<IteratorType>().key())>::type>::type;

    private:
        /// the iterator
        IteratorType anchor{};
        /// an index for arrays (used to create key names)
        std::size_t array_index = 0;
        /// last stringified array index
        mutable std::size_t array_index_last = 0;
        /// a string representation of the array index
        mutable string_type array_index_str = "0";
        /// an empty string (to return a reference for primitive values)
        string_type empty_str{};

    public:
        explicit iteration_proxy_value() = default;
        explicit iteration_proxy_value(IteratorType it, std::size_t array_index_ = 0) noexcept(std::is_nothrow_move_constructible<IteratorType>::value
                                                                                                       &&std::is_nothrow_default_constructible<string_type>::value)
            : anchor(std::move(it)), array_index(array_index_) {}

        iteration_proxy_value(iteration_proxy_value const &) = default;
        iteration_proxy_value &operator=(iteration_proxy_value const &) = default;
        // older GCCs are a bit fussy and require explicit noexcept specifiers on defaulted functions
        iteration_proxy_value(iteration_proxy_value &&) noexcept(std::is_nothrow_move_constructible<IteratorType>::value
                                                                         &&std::is_nothrow_move_constructible<string_type>::value) = default;
        iteration_proxy_value &operator=(iteration_proxy_value &&) noexcept(std::is_nothrow_move_assignable<IteratorType>::value
                                                                                    &&std::is_nothrow_move_assignable<string_type>::value) = default;
        ~iteration_proxy_value() = default;

        /// dereference operator (needed for range-based for)
        const iteration_proxy_value &operator*() const {
            return *this;
        }

        /// increment operator (needed for range-based for)
        iteration_proxy_value &operator++() {
            ++anchor;
            ++array_index;

            return *this;
        }

        iteration_proxy_value operator++(int) & // NOLINT(cert-dcl21-cpp)
        {
            auto tmp = iteration_proxy_value(anchor, array_index);
            ++anchor;
            ++array_index;
            return tmp;
        }

        /// equality operator (needed for InputIterator)
        bool operator==(const iteration_proxy_value &o) const {
            return anchor == o.anchor;
        }

        /// inequality operator (needed for range-based for)
        bool operator!=(const iteration_proxy_value &o) const {
            return anchor != o.anchor;
        }

        /// return key of the iterator
        const string_type &key() const {
            JSON_ASSERT(anchor.m_object != nullptr);

            switch (anchor.m_object->type()) {
                // use integer array index as key
                case value_t::array: {
                    if (array_index != array_index_last) {
                        int_to_string(array_index_str, array_index);
                        array_index_last = array_index;
                    }
                    return array_index_str;
                }

                // use key from the object
                case value_t::object:
                    return anchor.key();

                // use an empty key for all primitive types
                case value_t::null:
                case value_t::string:
                case value_t::boolean:
                case value_t::number_integer:
                case value_t::number_unsigned:
                case value_t::number_float:
                case value_t::binary:
                case value_t::discarded:
                default:
                    return empty_str;
            }
        }

        /// return value of the iterator
        typename IteratorType::reference value() const {
            return anchor.value();
        }
    };

    /// proxy class for the items() function
    template<typename IteratorType>
    class iteration_proxy {
    private:
        /// the container to iterate
        typename IteratorType::pointer container = nullptr;

    public:
        explicit iteration_proxy() = default;

        /// construct iteration proxy from a container
        explicit iteration_proxy(typename IteratorType::reference cont) noexcept
            : container(&cont) {}

        iteration_proxy(iteration_proxy const &) = default;
        iteration_proxy &operator=(iteration_proxy const &) = default;
        iteration_proxy(iteration_proxy &&) noexcept = default;
        iteration_proxy &operator=(iteration_proxy &&) noexcept = default;
        ~iteration_proxy() = default;

        /// return iterator begin (needed for range-based for)
        iteration_proxy_value<IteratorType> begin() const noexcept {
            return iteration_proxy_value<IteratorType>(container->begin());
        }

        /// return iterator end (needed for range-based for)
        iteration_proxy_value<IteratorType> end() const noexcept {
            return iteration_proxy_value<IteratorType>(container->end());
        }
    };

    // Structured Bindings Support
    // For further reference see https://blog.tartanllama.xyz/structured-bindings/
    // And see https://github.com/nlohmann/json/pull/1391
    template<std::size_t N, typename IteratorType, enable_if_t<N == 0, int> = 0>
    auto get(const nlohmann::detail::iteration_proxy_value<IteratorType> &i) -> decltype(i.key()) {
        return i.key();
    }
    // Structured Bindings Support
    // For further reference see https://blog.tartanllama.xyz/structured-bindings/
    // And see https://github.com/nlohmann/json/pull/1391
    template<std::size_t N, typename IteratorType, enable_if_t<N == 1, int> = 0>
    auto get(const nlohmann::detail::iteration_proxy_value<IteratorType> &i) -> decltype(i.value()) {
        return i.value();
    }

} // namespace detail
NLOHMANN_JSON_NAMESPACE_END

// The Addition to the STD Namespace is required to add
// Structured Bindings Support to the iteration_proxy_value class
// For further reference see https://blog.tartanllama.xyz/structured-bindings/
// And see https://github.com/nlohmann/json/pull/1391
namespace std {

#if defined(__clang__)
// Fix: https://github.com/nlohmann/json/issues/1401
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmismatched-tags"
#endif
    template<typename IteratorType>
    class tuple_size<::nlohmann::detail::iteration_proxy_value<IteratorType>>
        : public std::integral_constant<std::size_t, 2> {};

    template<std::size_t N, typename IteratorType>
    class tuple_element<N, ::nlohmann::detail::iteration_proxy_value<IteratorType>> {
    public:
        using type = decltype(get<N>(std::declval<
                                     ::nlohmann::detail::iteration_proxy_value<IteratorType>>()));
    };
#if defined(__clang__)
#pragma clang diagnostic pop
#endif

} // namespace std

#if JSON_HAS_RANGES
template<typename IteratorType>
inline constexpr bool ::std::ranges::enable_borrowed_range<::nlohmann::detail::iteration_proxy<IteratorType>> = true;
#endif
